Do not build file and image URLs by hand

Pitfall
linkImage and linkFile now cover every linking case, including a file stored on another document, so there is no longer a reason to assemble the four-segment URL yourself. Hand-built links skip the encoding and slugifying the functions do, make the output format easy to get wrong, and are invisible to the image-size Scan — so under a strict size policy they can return HTTP 400 in production.
Applies to: Hash templatesImagesFiles

What you'll see

You are linking an embedded image or file from template code and you write the URL out by hand, usually because the item lives on another document — a row in a listing rather than the page you are rendering:

<img src="#f.Url#/#f.Image1#/500x400x1/image.jpg" alt="#f.Title#" />
<a href="#f.Url#/#f.File1#/rapport.pdf" download>Download</a>

It renders. Then one of these happens: a title containing a space or æ ø å produces a broken link, a .webp name is served as JPEG anyway, or the site is switched to a strict image-size policy and the images start returning HTTP 400 — even though the same sizes work everywhere else on the site.

What's actually happening

Docly serves an embedded file from a path built out of the document URL, the embedded field value, an optional scaling instruction, and a display name:

<document-url>/<embedded-id>/<width>x<height>x<mode>/<display-name>.<ext>

Writing that string yourself used to be the only option for a file on another document, because linkImage and linkFile only knew the document being rendered. That is no longer true. Both functions now take a file object as an optional first argument and prepend its Url, so the row object from getFiles/getFolder is all you need:

linkImage(embeddedId, w, h [, mode] [, filename])
linkImage(fileObject, embeddedId, w, h [, mode] [, filename])

linkFile(embeddedId [, filename])
linkFile(fileObject, embeddedId [, filename])

The second reason for hand-building is gone too: both functions used to be registered only in a display template rendering a .docly document, so in a .hash page — exactly where listings live — calling them failed and a hand-written URL was the only way. They are now available in every #JS context: display templates, .hash pages, master pages and API scripts. Outside a display template there is no “current document”, so the id-only form has nothing to resolve against and fails with a message pointing at the file-object form — which is the form you want in a listing anyway.

With the last reason gone, a hand-built URL is now pure cost:

  • Encoding is yours to get wrong. The functions URL-encode the display name and turn spaces into hyphens. A raw #f.Title# in a hand-built path breaks at the first space, and Norwegian characters need encodeURIComponent that is easy to forget on one of the five places you copied the pattern to.
  • The extension decides the output format. For images the display name's extension selects the conversion (.webp, .avif, .png, .jpg). That is a real behaviour, not decoration, and it is documented on linkImage — a hand-written name detaches the choice from the documentation and from the format rules (WebP/AVIF need images.sizePolicy auto or strict; under the default open the same link is served as JPEG).
  • The size Scan cannot see it. The folder's Image sizes → Scan discovers the sizes your site uses by finding literal linkImage calls in your source. A WxHxM string you concatenated into an <img src> is not a linkImage call, so the size never lands in allowedSizes/discoveredSizes. Under images.sizePolicy: "strict" an unregistered size is refused with HTTP 400 — the failure shows up when the policy is tightened, long after the template was written.
  • You are coupled to the URL shape. Path slugifying, double-slash collapsing and # escaping all happen inside the functions. Template code that reimplements the format inherits every future change to it, and the bugs are silent: a link that 404s only for the items whose titles happen to contain the wrong character.

What to do

1. A file or image on the current document. Pass the field value; add a descriptive display name.

<img src="#docly.linkImage(Image1, 1200, 800, 0, 'produktnavn.webp')#" width="1200" height="800" alt="...">
<a href="#docly.linkFile(File1, 'arsrapport-2025.pdf')#" download>Download</a>

2. A file or image on another document — pass the row object. This is the case that used to force hand-built URLs. Give the function the item itself as the first argument and it prepends item.Url for you:

#let items = docly.getFiles('~/artikler');#
#for (let f of items) {#
  <img src="#docly.linkImage(f, f.Image1, 500, 400, 1, f.Title + '.webp')#"
       width="500" height="400" alt="#enc(f.Title)#">
  <a href="#docly.linkFile(f, f.File1, f.Title + '.pdf')#" download>#enc(f.Title)#</a>
#}#

Note that you pass the raw title — no encodeURIComponent. The functions encode it and swap spaces for hyphens.

The row must carry a Url, which is what getFiles/getFolders return by default. getFiles(path, name, depth, 'min') omits it to keep the result small, and the call then fails rather than emitting a broken link — use the default full (or medium) mode when you intend to link the rows.

3. Options-object syntax works on both overloads. Use it when the argument list gets long or you only want to set some of it:

docly.linkImage(f.Image1, { width: 1200, height: 800, filename: 'produkt.webp' })
docly.linkImage(f, f.Image1, { width: 500, height: 400, mode: 1, filename: f.Title + '.webp' })
docly.linkFile(f, f.File1, { filename: f.Title + '.pdf' })

4. CSS background images still need the tilde. The function returns the link; the leading ~ is what makes Docly rewrite it at publish time:

<div class="hero" data-bg="~#docly.linkImage(Image1, 1600, 900, 1, 'hero.webp')#"></div>

5. Migrating an existing template. Search your hash and template files for Url#/ and for literal size segments (x0/, x1/, x2/). Each hit is one of the two forms above. After converting, run the folder's Image sizes → Scan so the now-visible sizes are registered before you tighten images.sizePolicy.

6. The one case that remains manual: URLs built in client-side JavaScript. A link assembled in the browser cannot use these functions, so it also cannot be discovered by the Scan. Register those sizes in allowedSizes in site.json yourself, and encodeURIComponent the display name. Keep this to the few places that genuinely need it — everything rendered server-side should go through linkImage/linkFile. See Restrict and register image sizes.